﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Jacaranda.Model
{
    public class InClause : ConditionalExpression
    {
        public InClause()
        {
            IsDemandingSource = true;
        }

        internal InClause Child { get; set; }

        private Type demandedType;
        internal Type DemandedType
        {
            get { return demandedType; }
            set
            {
                demandedType = value;

                if (Child != null)
                {
                    Child.DemandedType = IsDemandingSource ? Association.To : Association.From;
                }
            }
        }

        internal Type Type1 { get; set; }

        internal Type Type2 { get; set; }

        internal int Instance { get; set; }

        internal string TopMostKeyFieldName { get; set; }

        internal ConditionalExpression Condition1 { get; set; }

        internal ConditionalExpression Condition2 { get; set; }

        internal bool IsTopMost { get; set; }

        internal bool IsDemandingSource { get; set; }

        private InClause Innermost
        {
            get
            {
                InClause clause = this;
                while (clause.Child != null)
                {
                    clause = clause.Child;
                }

                return clause;
            }
        }

        public InClause In(Type type1, Type type2, ConditionalExpression condition1, ConditionalExpression condition2)
        {
            Innermost.Child = new InClause()
            {
                Type1 = type1,
                Type2 = type2,
                Condition1 = condition1,
                Condition2 = condition2,
                IsDemandingSource = true,
            };

            return this;
        }

        public InClause In<T1, T2>(ConditionalExpression condition1, ConditionalExpression condition2)
        {
            Innermost.Child = new InClause()
            {
                Type1 = typeof(T1),
                Type2 = typeof(T2),
                Condition1 = condition1,
                Condition2 = condition2,
                IsDemandingSource = true,
            };

            return this;
        }

        public InClause In<T1, T2>(ConditionalExpression condition1)
        {
            return In<T1, T2>(condition1, null);
        }

        public InClause In<T1, T2>()
        {
            return In<T1, T2>(null, null);
        }

        private InClause In<T1, T2>(string associationPropertyName, bool isDemandingSource, ConditionalExpression condition1, ConditionalExpression condition2)
        {
            Innermost.Child = new InClause()
            {
                Type1 = typeof(T1),
                Type2 = typeof(T2),
                Instance = Metadata.Type(typeof(T1)).AssociationFields.First(i => i.PropertyName == associationPropertyName).Association.Instance,
                Condition1 = condition1,
                Condition2 = condition2,
                IsDemandingSource = isDemandingSource
            };

            return this;
        }

        public InClause In<T1, T2>(string associationPropertyName, ConditionalExpression condition1, ConditionalExpression condition2)
        {
            if (typeof(T1).Equals(typeof(T2)))
            {
                throw new InvalidOperationException("Jacaranda.Database.In: T1 and T2 cannot be the same. Call InSource or InTarget instead.");
            }

            return In<T1, T2>(associationPropertyName, true, condition1, condition2);
        }

        public InClause InSource<T1, T2>(string associationPropertyName, ConditionalExpression condition1, ConditionalExpression condition2)
        {
            if (!typeof(T1).Equals(typeof(T2)))
            {
                throw new InvalidOperationException("Jacaranda.Database.InSource: T1 and T2 must be the same. Call In instead.");
            }

            return In<T1, T2>(associationPropertyName, true, condition1, condition2);
        }

        public InClause InSource<T1, T2>(string associationPropertyName)
        {
            return InSource<T1, T2>(associationPropertyName, null, null);
        }

        public InClause InTarget<T1, T2>(string associationPropertyName, ConditionalExpression condition1, ConditionalExpression condition2)
        {
            if (!typeof(T1).Equals(typeof(T2)))
            {
                throw new InvalidOperationException("Jacaranda.Database.InTarget: T1 and T2 must be the same. Call In instead.");
            }

            return In<T1, T2>(associationPropertyName, false, condition1, condition2);
        }

        public InClause InTarget<T1, T2>(string associationPropertyName)
        {
            return InTarget<T1, T2>(associationPropertyName, null, null);
        }


        public InClause No<T1, T2>()
        {
            Innermost.Child = new NoClause()
            {
                Type1 = typeof(T1),
                Type2 = typeof(T2),
            };

            return this;
        }


        private InClause First()
        {
            InClause first = this;

            while (first.Child != null)
            {
                first = first.Child;
            }
            return first;
        }


        private string CreateConditionExpression(EntityMetadata m, string key, ConditionalExpression condition)
        {
            bool isPrimaryKeyMatchCondition = condition is FieldMatchCriteria &&
                (condition as FieldMatchCriteria).Field is AbstractProperty &&
                m.Fields.FirstOrDefault(i => i.PropertyName == ((condition as FieldMatchCriteria).Field as AbstractProperty).Name) != null &&
                m.Fields.FirstOrDefault(i => i.PropertyName == ((condition as FieldMatchCriteria).Field as AbstractProperty).Name).IsPrimaryKey &&
                (condition as FieldMatchCriteria).Operator == BinaryOperator.Equals;

            if (isPrimaryKeyMatchCondition)
            {
                return string.Format("{0} = '{1}'", key, (condition as FieldMatchCriteria).Value);
            }
            else
            {
                return string.Format("{0} IN (SELECT {1} FROM {2} WHERE {3})",
                                          key,
                                          m.PrimaryKey.FieldName,
                                          m.TableName,
                                          condition.GenerateSql(m.Type));
            }
        }

        private string CreateConditionalClause()
        {
            EntityMetadata entity1 = Metadata.Type(Type1);
            EntityMetadata entity2 = Metadata.Type(Type2);

            if (Condition1 == null && Condition2 == null)
            {
                return null;
            }
            else if (Condition1 != null && Condition2 == null)
            {
                StringBuilder b = new StringBuilder();

                foreach (EntityMetadata m in entity1.RelatedEntities)
                {
                    b.Append(CreateConditionExpression(m, "Key1", Condition1));
                    b.Append(" OR ");
                }

                return b.ToString().Substring(0, b.ToString().Length - 4);
            }
            else if (Condition1 == null && Condition2 != null)
            {
                StringBuilder b = new StringBuilder();

                foreach (EntityMetadata m in entity2.RelatedEntities)
                {
                    b.Append(CreateConditionExpression(m, "Key2", Condition2));
                    b.Append(" OR ");
                }

                return b.ToString().Substring(0, b.ToString().Length - 4);
            }
            else if (Condition1 != null && Condition2 != null)
            {
                StringBuilder b = new StringBuilder();

                foreach (EntityMetadata m1 in entity1.RelatedEntities)
                {
                    foreach (EntityMetadata m2 in entity2.RelatedEntities)
                    {
                        b.Append(string.Format("{0} AND {1}", CreateConditionExpression(m1, "Key1", Condition1), CreateConditionExpression(m2, "Key2", Condition2)));
                        b.Append(" OR ");
                    }
                }

                return b.ToString().Substring(0, b.ToString().Length - 4);
            }

            throw new Exception();
        }


        private EntityAssociation association;
        protected EntityAssociation Association
        {
            get
            {
                if (association == null)
                {
                    association = Metadata.Associations.FirstOrDefault(i => i.From.Equals(Type1) && i.To.Equals(Type2) && i.Instance == Instance);
                    if (association == null)
                    {
                        association = Metadata.Associations.First(i => i.From.IsAssignableFrom(Type1) && i.To.IsAssignableFrom(Type2) && i.Instance == Instance);
                    }
                }

                return association;
            }
        }

        protected bool IsForwardDirection
        {
            get
            {
                if (Association.From.Equals(Association.To))
                {
                    return IsDemandingSource;
                }
                else
                {
                    return Association.From.Equals(DemandedType);
                }
            }
        }


        internal override string GenerateSql(Type propertyEntityType)
        {
            string keyToSelect = IsForwardDirection ? "Key1" : "Key2";
            string keyToAssociate = IsForwardDirection ? "Key2" : "Key1";

            string condition = CreateConditionalClause();

            if (Child == null)
            {
                if (string.IsNullOrEmpty(condition))
                {
                    return string.Format("{0} IN (SELECT {1} FROM {2})",
                        IsTopMost ? TopMostKeyFieldName : null,
                        keyToSelect, 
                        Association.TableName);
                }
                else
                {
                    return string.Format("{0} IN (SELECT {1} FROM {2} WHERE ({3}))",
                        IsTopMost ? TopMostKeyFieldName : null,
                        keyToSelect,
                        Association.TableName,
                        condition);
                }
            }
            else
            {
                if (string.IsNullOrEmpty(condition))
                {
                    return string.Format("{0} IN (SELECT {1} FROM {2} WHERE {3} {4})",
                        IsTopMost ? TopMostKeyFieldName : null,
                        keyToSelect,
                        Association.TableName,
                        keyToAssociate,
                        Child.GenerateSql(null));
                }
                else
                {
                    return string.Format("{0} IN (SELECT {1} FROM {2} WHERE ({3}) AND {4} {5})",
                    IsTopMost ? TopMostKeyFieldName : null,
                    keyToSelect,
                    Association.TableName,
                    condition,
                    keyToAssociate,
                    Child.GenerateSql(null));
                }
            }
        }
    }
}
